﻿// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
// Copyright (c) 2011, Dr. Masroor Ehsan
// All rights reserved.
// 
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//     * Redistributions of source code must retain the above copyright
//       notice, this list of conditions and the following disclaimer.
//     * Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//     * Neither the name of the author nor the
//       names of its contributors may be used to endorse or promote products
//       derived from this software without specific prior written permission.
// 
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
// DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
// 
// $Author:$
// $Id:$
// $Rev:$
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

using System;

namespace MaxTrader.Core
{
    public struct Money : IEquatable<Money>,
                          IComparable<Money>,
                          IFormattable,
                          IConvertible
    {
        /// <summary>
        /// The amount - stored by NHibernate
        /// </summary>
        private readonly decimal _amount;
        
        /// <summary>
        /// Currency code - stored by NHibernate
        /// </summary>
        private readonly int _currencyIsoCode;

        /// <summary>
        /// The currency - this property is for caching purpose only.
        /// It is not saved in the database by NHibernate.
        /// </summary>
        private Currency? _currency;

        public Money(Decimal amount)
        {
            _currency = null;
            checkValue(amount);
            _amount = amount;
            _currencyIsoCode = Currency.FromCurrentCulture().IsoNumericCode;
        }

        public Money(Decimal amount, int currencyIsoCode)
        {
            _currency = null;
            checkValue(amount);
            _amount = amount;
            _currencyIsoCode = currencyIsoCode;
        }

        public Money(Decimal amount, Currency currency)
        {
            _currency = null;
            checkValue(amount);
            _amount = amount;
            _currencyIsoCode = currency.IsoNumericCode;
        }

        public Currency Currency
        {
            get 
            {
                if (_currency == null) _currency = new Currency(_currencyIsoCode);
                return (Currency) _currency;
            }
        }

        public int CurrencyIsoCode
        {
            get { return _currencyIsoCode; }
        }

        public decimal Amount
        {
            get { return _amount; }
        }

        #region Implementation of IEquatable<Money>

        public Boolean Equals(Money other)
        {
            checkCurrencies(other);
            return _amount == other.Amount;
        }

        #endregion

        #region Implementation of IComparable<Money>

        public int CompareTo(Money other)
        {
            checkCurrencies(other);
            if (this < other)
            {
                return -1;
            }
            if (this > other)
            {
                return 1;
            }
            return 0;
        }

        #endregion

        #region Implementation of IFormattable

        public String ToString(String format, IFormatProvider formatProvider)
        {
            return _amount.ToString(format, formatProvider);
        }

        #endregion

        #region Implementation of IConvertible

        public TypeCode GetTypeCode()
        {
            return TypeCode.Object;
        }

        public Boolean ToBoolean(IFormatProvider provider)
        {
            return _amount == 0;
        }

        public Char ToChar(IFormatProvider provider)
        {
            throw new NotSupportedException();
        }

        public SByte ToSByte(IFormatProvider provider)
        {
            return (SByte) _amount;
        }

        public Byte ToByte(IFormatProvider provider)
        {
            return (Byte) _amount;
        }

        public Int16 ToInt16(IFormatProvider provider)
        {
            return (Int16) _amount;
        }

        public UInt16 ToUInt16(IFormatProvider provider)
        {
            return (UInt16) _amount;
        }

        public Int32 ToInt32(IFormatProvider provider)
        {
            return (Int32) _amount;
        }

        public UInt32 ToUInt32(IFormatProvider provider)
        {
            return (UInt32) _amount;
        }

        public Int64 ToInt64(IFormatProvider provider)
        {
            return (Int64) _amount;
        }

        public UInt64 ToUInt64(IFormatProvider provider)
        {
            return (UInt64) _amount;
        }

        public Single ToSingle(IFormatProvider provider)
        {
            return (Single) _amount;
        }

        public Double ToDouble(IFormatProvider provider)
        {
            return (Double) _amount;
        }

        public Decimal ToDecimal(IFormatProvider provider)
        {
            return _amount;
        }

        public DateTime ToDateTime(IFormatProvider provider)
        {
            throw new NotSupportedException();
        }

        public String ToString(IFormatProvider provider)
        {
            return ((Decimal) this).ToString(provider);
        }

        public Object ToType(Type conversionType, IFormatProvider provider)
        {
            throw new NotSupportedException();
        }

        #endregion

        public static implicit operator Money(Byte value)
        {
            return new Money(value);
        }

        public static implicit operator Money(SByte value)
        {
            return new Money(value);
        }

        public static implicit operator Money(Single value)
        {
            return new Money((Decimal) value);
        }

        public static implicit operator Money(Double value)
        {
            return new Money((Decimal) value);
        }

        public static implicit operator Money(Decimal value)
        {
            return new Money(value);
        }

        public static implicit operator Decimal(Money value)
        {
            return value._amount;
        }

        public static implicit operator Money(Int16 value)
        {
            return new Money(value);
        }

        public static implicit operator Money(Int32 value)
        {
            return new Money(value);
        }

        public static implicit operator Money(Int64 value)
        {
            return new Money(value);
        }

        public static implicit operator Money(UInt16 value)
        {
            return new Money(value);
        }

        public static implicit operator Money(UInt32 value)
        {
            return new Money(value);
        }

        public static implicit operator Money(UInt64 value)
        {
            return new Money(value);
        }

        public static Money operator -(Money value)
        {
            return new Money(-value._amount, value._currencyIsoCode);
        }

        public static Money operator +(Money left, Money right)
        {
            if (left.Currency != right.Currency)
            {
                throw differentCurrenciesException();
            }

            return new Money(left._amount + right._amount,
                             left.Currency);
        }

        public static Money operator -(Money left, Money right)
        {
            if (left.Currency != right.Currency)
            {
                throw differentCurrenciesException();
            }

            return new Money(left._amount - right._amount,
                             left.Currency);
        }

        public static Money operator *(Money left, Decimal right)
        {
            return new Money(Decimal.Floor(left._amount*right), left._currencyIsoCode);
        }

        public static Money operator /(Money left, Decimal right)
        {
            return new Money(left._amount / right, left._currencyIsoCode);
        }

        public static Boolean operator ==(Money left, Money right)
        {
            return left.Equals(right);
        }

        public static Boolean operator !=(Money left, Money right)
        {
            return !left.Equals(right);
        }

        public static Boolean operator >(Money left, Money right)
        {
            return left.CompareTo(right) > 0;
        }

        public static Boolean operator <(Money left, Money right)
        {
            return left.CompareTo(right) < 0;
        }

        public static Boolean operator >=(Money left, Money right)
        {
            return left.CompareTo(right) >= 0;
        }

        public static Boolean operator <=(Money left, Money right)
        {
            return left.CompareTo(right) <= 0;
        }

        public override Int32 GetHashCode()
        {
            return 207501131 ^ _amount.GetHashCode() ^ _currencyIsoCode.GetHashCode();
        }

        public override Boolean Equals(Object obj)
        {
            if (!(obj is Money))
            {
                return false;
            }

            var other = (Money) obj;
            return Equals(other);
        }

        public override String ToString()
        {
            return _amount.ToString("C");
        }

        public String ToString(String format)
        {
            return _amount.ToString(format);
        }

        private static Exception differentCurrenciesException()
        {
            return new InvalidOperationException("Money values are in different " +
                                                 "currencies. Convert to the same " +
                                                 "currency before performing " +
                                                 "operations on the values.");
        }

        private static void checkValue(Decimal value)
        {
            if (value < Int64.MinValue || value > Int64.MaxValue)
            {
                throw new ArgumentOutOfRangeException("value",
                                                      value,
                                                      "Money amount must be between " +
                                                      Int64.MinValue + " and " +
                                                      Int64.MaxValue);
            }
        }

        private void checkCurrencies(Money other)
        {
            if (other._currencyIsoCode != _currencyIsoCode)
            {
                throw differentCurrenciesException();
            }
        }
    }
}